package corpustools;
import swutils.BinaryOut;
import java.util.Set;
import java.util.TreeSet;
import java.util.Queue;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.io.ByteArrayOutputStream;

/** Callback defining action to be taken when the automaton finds a match. */
class CorruptionDetection implements strings.MatchAction {

    /** Parameter to determine what kinds of corruptions to show. */
    private boolean showUnexpected = true;

    /** Queue of elements that have already passed through the
     * rightContextQ. These may still be needed as left context when a
     * corruption is removed from the rightContextQ. */
    private Queue<String> leftContextQ = new LinkedList<String>();

    /** Queue storing elements that are waiting to become the "key word
     * in context. **/
    private LinkedList<String> rightContextQ = new LinkedList<String>();

    /** Positions of elements on rightContextQ. */
    private LinkedList<Integer> positionQ = new LinkedList<Integer>();

    /** The position.  */
    private int n = 0;

    /** Queue to keep record of original text. */
    private Queue<Character> text = new LinkedList<Character>();

    /** Queue parallel to rightContextQ to indicate whether elements
     * added to tat queue are corruptions. */
    private Queue<Boolean> corruptionQ = new LinkedList<Boolean>();

    /** Controls the spacing around the key word in the concordance. */
    private int radius;

    /** The unexpected blocks. */
    private Set<Character.UnicodeBlock> blocks;

    /** The unexpected blocks along with the set of characters. */
    private Map<Character.UnicodeBlock, Set<Character>> unexpectedBlocks;

    /** Outputs of automaton. See constructor for <code>ValidUtf8</code>. */
    private List<List<String>> outputs;


    /** Position of the last print. */
    private int lastPosition = 0;

    /** Length of the last print. */
    private int lastLength = 1;

    /** A destination for binary printing. */
    private ByteArrayOutputStream lastBaos = new ByteArrayOutputStream();

    /** Used in <code>action</code> to "print" into an array, allowing
     * the fina printing to the screen to be delayed. */
    private BinaryOut lastBinOut = new BinaryOut(lastBaos);



    /**
     * Constructor.
     * @param radius0 the length of left and right context on each line
     * @param blocks0 the unexpected blocks, probable corruptions
     * @param outputs0 The desired outputs
     */
    CorruptionDetection(int radius0, Set<Character.UnicodeBlock> blocks0,
                        List<List<String>> outputs0) {
        this.radius = radius0;
        this.blocks = blocks0;
        this.outputs = outputs0;
        unexpectedBlocks = new HashMap<Character.UnicodeBlock,
            Set<Character>>();
    }

    /**
     * An accessor to the unexpected Unicode blocks, which are
     * probably corruptions.
     * @return the unexpected blocks, along with the corresponding set
     * of characters.
     */
    Map<Character.UnicodeBlock, Set<Character>> getUnexpected() {
        return unexpectedBlocks;
    }




    /**
     * Ensure that the left context breaks at a word boundary.
     * @param left the left conntext
     * @return the trimmed left context
     */
    String trimLeft(String left) {
        if (left.length() < radius) {
            return left;
        }
        int ind = left.indexOf(' ');
        if (ind != -1) {
            left = left.substring(ind + 1);
        } else {
            return left;
        }
        return trimLeft(left);
    }


    /**
     * Ensure that the right context breaks at a word boundary.
     * @param right the right conntext
     * @return the trimmed right context
     */
    String trimRight(String right) {
        int ind = right.lastIndexOf(' ');
        if (ind != -1) {
            right = right.substring(0, ind);
        }
        return right;
    }



    /**
     * Do a little extra normalization to show what kind of newline is
     * used. I tried to use symbols as in the Yudit editor, but not
     * everyting was available in Unicode.
     * @param str the string (1 or 2 characters) to be normalized.
     * @return the normalization of str
     */
    static String normalize(String str) {
        /* Using visible space is a possibility
        if (str.equals(" ")) {
            return "␣";
        }
        */

        if (str.equals("\n")) {
            return "";
        }
        if (str.equals("\r")) {
            return "↵";
        }

        if (str.equals("\r\n")) {

           return "⌋";
        }
        return str;
    }

    /**
     * At the end, when there is no more input, the elements on the
     * waiting list can be printed out.  Also, remove the
     * sentinel. The purpose of the sentinel was to ensure that a
     * corruption at the end of the text will be found. At this point,
     * the sentinel has served its purpose and can be removed.
     */
    public void flush() {
        rightContextQ.removeLast();
        while (rightContextQ.size() > 1) {
            concordanceLine();
        }
    }

    /**
     * Keep a record of original text.
     * @param ch the character to remember
     */
    void rememberText(char ch) {
        text.add(ch);
    }

    /**
     * reset the counter for a new search.
     */
    public void reset() {
        n = 0;
    }

    /**
    * The main difficulty is that when corruptions are discovered,
    * there must be a delay before they are reported. This method
    * manages the delay. Corruptions and non-corruptions are stored
    * on queues.
    * @param right element to be added to the queue
    * @param isCorruption true is the element added is a corruption
    * @param position a text position
    */
    void addToQueue(String right, boolean isCorruption, int position) {

        rightContextQ.add(normalize(right));
        corruptionQ.add(isCorruption);
        positionQ.add(position);

        // We have enough right context. If there is something to
        // report, we can do so. Otherwise just transfer the overflow
        // element from the right context to the left context.
        if (rightContextQ.size() > radius) {
            concordanceLine();
        }
    }


    /**
     * The concordance requires a delay mechanism. When a corruption
     * is found, it is necessary to wait for a little right context to
     * be found. The waiting list is called
     * <code>rightContextQ</code>. When an element gets to the front of
     * the queue, it is dealt with.  If it's a corruption print it
     * out. In either case transfer an element from right context to
     * left context.
     */
    void concordanceLine() {
        String center = rightContextQ.remove();
        n++;
        boolean isCorruption = corruptionQ.remove();
        // int position = positionQ.remove();

        if (isCorruption) {
            StringBuilder left = new StringBuilder();
            for (String lStr : leftContextQ) {
                left.append(lStr);
            }
            System.out.printf("%6d %" + radius + "s  %-10s  ",
                              n,   // 4 spaces for position (enough?)
                              trimLeft(left.toString()),  // left radius
                              center);   // 10 spaces for corruption

            int rightLen = 0;
            for (String rStr : rightContextQ) {
                rightLen += rStr.length();
                if (rightLen > radius && rStr.matches("[ ⏎↵⌋]")) {
                    break;
                }
                System.out.print(rStr);
            }
            System.out.println();
        }

        // Transfer element to left context.
        leftContextQ.add(center);
        if (leftContextQ.size() > radius) {
            leftContextQ.remove();
        }
    }




    /**
     * A callback method. Used as follows: created in ValidateUtf8 and
     * called in Utf8NormalizedInput. The idea is that the normalizer
     * makes major changes to the text by putting the characters into
     * equivalence classes. But in order to create a concordance to
     * show the user where the corruptions occur, it is necessary to
     * remember the original text and how this got normalized. So the
     * Utf8NormalizedInput class is required to call this
     * <code>action</code> method whenever a sequence of bytes is normalized.
     * @param position a text pointer
     * @param outputId the output of the normalizer
     */
    public void action(int position, int outputId) {
        List<String> output = outputs.get(outputId);
        int length = output.size();
        int lengthChange = length - lastLength;
        boolean newIntervalContainsOld =
            ((position - lastPosition) <= lengthChange);
        boolean newIntervalContainedByOld =
            ((position == lastPosition)  && (length < lastLength));
        int gapSize = (position - length + 1) - lastPosition;

        lastPosition = position;
        lastLength = length;

        if (newIntervalContainsOld) {
            int p = positionQ.removeLast();
            positionQ.add(p + lengthChange);

            for (int i = 0; i < lengthChange; i++) {
                lastBinOut.write(text.remove());
            }
            lastBinOut.flush();
            String str = lastBaos.toString();
            rightContextQ.removeLast();
            rightContextQ.add(normalize(str));
            return;
        }
        if (newIntervalContainedByOld) {
            return;
        }

        if (gapSize > 1) {
            // Store corruption in String representing sequence
            // of hexadecimal values
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < gapSize - 1; i++) {
                sb.append(String.format("%x ", (int) text.remove()));
            }
            // We found a corruption, but we can't print it out yet
            // because we haven't yet seen the right context.
            String centerC = sb.toString();
            // positionQ.add(position);
            addToQueue(" " + centerC, true, position - length);
        }


        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BinaryOut out = new BinaryOut(baos);
        for (int i = 0; i < length; i++) {
            out.write(text.remove());
        }
        out.flush();
        String str = baos.toString();
        // save these values in case we have to go back and make a
        // correction for a longer match
        lastBaos = baos;
        lastBinOut = out;

        // check whether unusual Unicode blocks are used
        Character.UnicodeBlock block =
            Character.UnicodeBlock.of(Character.codePointAt(str, 0));
        if (block == null) {
            // If the block is null, then the character is definately
            // undefined. Alternatively, Character.isDefined() or
            // (Character.getType(c) != Character.UNASSIGNED) can be
            // used, but these appear to miss quite a few newer
            // Unicode characters.

            addToQueue(str, true, position);

        } else if (!blocks.contains(block)) {

            // optionally treat unusual Unicode blocks as corruptions
            addToQueue(str + block, showUnexpected, position);
            Set<Character> letters = unexpectedBlocks.get(block);
            if (letters == null) {
                letters = new TreeSet<Character>();
                unexpectedBlocks.put(block, letters);
            }
            letters.add(str.charAt(0));
        } else {
            addToQueue(str, false, position);
        }
    }
}
